package p2pmodule;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import com.kenmccrary.jtella.DownloadConstants;
import com.kenmccrary.jtella.GNUTellaConnection;
import com.kenmccrary.jtella.MessageReceiverAdapter;
import com.kenmccrary.jtella.NodeConnection;
import com.kenmccrary.jtella.SearchMessage;
import com.kenmccrary.jtella.SearchReplyMessage;

import dustlog.LogController;

/** TODO: Should it be an independent thread?. **/
/**
 * When the GnutellaServant receive a QUERY message it will create an
 * instance of this class and it will be the responsible of handle
 * everything around that QUERY message. 
 * 
 * @author ole
 */
public class QueryHandler extends MessageReceiverAdapter {
	// Class attributes.
	private GNUTellaConnection conn = null;
	private FileServer server = null;
	
	/*
	 * Private classes.
	 */
	
	/**
	 * This class is used to filter all the files in a directory that contains
	 * (or not) the supplied pattern in the name.
	 */
	private class PatternFilter implements FilenameFilter {
		// Class attributes.
		private String pattern = null;
		
		/**
		 * Class constructor. Will create the filter based on the pattern
		 * passed. If the the pattern passed is null then every file will
		 * match.
		 * 
		 * This pattern is literally matched, it is not a regex.
		 * 
		 * @param pattern:
		 * 		The pattern to be used when filtering files.
		 */
		public PatternFilter(String pattern) {
			this.pattern = pattern;
		}
		
		@Override
		public boolean accept(File dir, String filename) {
			// If the pattern is null we consider that the file match. Don't
			// use it unless you know what you are doing.
			if (pattern == null)
				return true;
			
			// Return what the match of the filename against the pattern
			// returns.
			return filename.contains(pattern);
		}
	}
	
	/*
	 * Private methods of class QueryHandler.
	 */
	
	/**
	 * Returns all the files in the shared directory that have pattern in their
	 * names.
	 * 
	 * @return:
	 * 		A List of Files that have pattern in their name. The array will
	 * 		be empty if there are no files with that pattern.
	 */
	private List<File> getSharedFilesFromPattern(String pattern) {
		List<File> matching_files;
		PatternFilter filter;
		String shared_dirname;
		File shared_dir;
		
		// Create the List and the filter for the listing.
		matching_files = new ArrayList<File>();
		filter = new PatternFilter(pattern);
		
		// Search inside the shared directory files that match the pattern.
		shared_dirname = server.getSharedDir();
		shared_dir = new File(shared_dirname);
		for (File matching_file: shared_dir.listFiles(filter))
			matching_files.add(matching_file);
		
		return matching_files;
	}
	
	/**
	 * Method that sends a SearchReplyMessage (a QUERYHIT) to the originator
	 * of the SearchMessage passed informing about the files we have that match
	 * its search criteria.
	 * 
	 * @param msg:
	 * 		The SearchMessage we are replying.
	 * @param files:
	 * 		The List of Files that matched the search criteria of the
	 * 		SearchMessage. 
	 */
	private void sendSearchReply(SearchMessage msg, List<File> files) {
		SearchReplyMessage reply_msg;
		SearchReplyMessage.FileRecord record;
		String ip_address;
		short server_port;

		// Create the QUERYHIT message.
		server_port = FileServer.PORT;
		/** TODO: Here we have a problem:
		 * 
		 * Firewalled systems: We are behind a firewall, maybe it will not let
		 * the client start the connection (PUSH messages to the rescue). We
		 * can have a private IP address and a NAT behind us. We cannot send a
		 * QUERYHIT message with our private address, we need to use the
		 * address of the WAN interface of our NAT system... in fact we need
		 * more than that, we need the public address of all our NAT system, it
		 * could be a more-than-once NAT system (you know NAT through NAT
		 * through NAT...).
		 * 
		 * N-interfaces systems: We have many IP address, which should be the
		 * one used for this QUERYHIT?.
		 * **/
		ip_address = msg.getOriginatingConnection().getSocket().getLocalAddress().toString().substring(1);
		reply_msg = new SearchReplyMessage(msg, server_port, ip_address,
										   DownloadConstants.DOWNLOADSPEED_DSL);
		
		// Fill the message with the files that matched the search criteria.
		// Despite the official jtella web page documentation, there is a
		// fourth parameter, sha1Hash. Right now we will not use it so we pass
		// the empty string. We cannot pass null because internally jtella will
		// try to read without checking that so we can incur in a
		// NullPointerException. Check jtella source code.
		for (File file: files) {
			record = new SearchReplyMessage.FileRecord(files.indexOf(file),
													   (int)file.length(),
													   file.getName(),
													   "");
			reply_msg.addFileRecord(record);
		}
		
		// Send the message to the originator peer.
		try {
			msg.getOriginatingConnection().send(reply_msg);
		}
		catch (IOException e) {
			LogController.log("Input/output exception while sending QUERYHIT" +
							  " message.");
		}
	}
	
	/**
	 * Sends a QUERY message to the rest of peers this node knows.
	 *  
	 * @param msg:
	 * 		The SearchMessage to send.
	 */
	private void reissueSearchMessage(SearchMessage msg) {
		String msg_orig_ip, node_ip;
		
		// For each connection to a peer in the Gnutella network reissue the
		// search message.
		msg_orig_ip = msg.getOriginatingConnection().getSocket().getLocalAddress().toString().substring(1);
		for (NodeConnection node_conn: conn.getConnectionList()) {
			
			// Check if the originator of the message is one of my peers and
			// don't reissue the search to him.
			node_ip = node_conn.getSocket().getLocalAddress().toString().substring(1);
			if (!node_ip.equals(msg_orig_ip)) {
				try {
					node_conn.send(msg);
				}
				catch (IOException e) {}
			}
		}
	}
	
	/*
	 * Public methods of class QueryHandler.
	 */
	
	/**
	 * Class constructor.
	 * 
	 * @param conn:
	 * 		The connection to the Gnutella network (a GNUTellaConnection)
	 * 		instance.
	 * @param server:
	 * 		The FileServer that will be associated to this QUERY.
	 */
	public QueryHandler(GNUTellaConnection conn, FileServer server) {
		this.conn = conn;
		this.server = server;
	}

	/**
	 * This method handles a QUERY message. It checks if we have files with
	 * the pattern searched by the message. If so we send a QUERYHIT to the
	 * client that is searching for those files. Elsewhere we do nothing.
	 */
	public void receiveSearch(SearchMessage msg) {
		String pattern;
		List<File> files;
		
		// Check if we have files with that pattern.
		pattern = msg.getSearchCriteria();
		files = getSharedFilesFromPattern(pattern);
		
		// We have files with that pattern, send a QUERYHIT message to the node
		// that started the QUERY. 
		if (!files.isEmpty())
			sendSearchReply(msg, files);
		
		// Reissue the message to the rest of peers.
		reissueSearchMessage(msg);
	}
}